package Tracer;

import java.awt.Point;
import java.util.*;

import XMLScene.RTCamera;
import XMLScene.RTImportXML;
import XMLScene.RTLight;
import XMLScene.RTIObject;

/**
 * ClassName : RTCoreTracing RTCoreTracing is the core calculation class for ray
 * tracing.
 * 
 * @version 1.0 Oct 5th 2009
 * @author LU LIU & PENG ZHANG
 */

public class RTCoreTracing implements Runnable {

	/**
	 * the object List in the world scene
	 */
	private Vector<RTIObject> objList;

	/**
	 * the camera instance
	 */
	private RTCamera camera;
	/**
	 * the light list
	 */
	private Vector<RTLight> lightList;
	/**
	 * the canvas between camera and world scene , used to project the 3D Image
	 */
	private RTCanvas canvas;
	/**
	 * array of pixels to be calculated and being put into the thread pool as a
	 * computing unit
	 */
	private Point[] pixelCoordinationArray;
	/**
	 * result static bitmap, written by multi-thread
	 */
	private RTBitmap resultBitmap;
	/**
	 * the background color
	 */
	private RTColor background;
	/**
	 * the parameter used to rectify the ray.
	 * 
	 * In order to prevent roundoff errors from putting the intersection point
	 * on the wrong side of the intersected surface, use a coefficient to make
	 * intersection point offset slightly in the direction away from the
	 * surface.
	 */
	private final double RECTIFYER = 0.0001;

	/**
	 * the cameraLocation used in getValidHitPoint function
	 */
	private RTVector cameraLocation = new RTVector();
	/**
	 * the hitpointObj used in getValidHitPoint function
	 */
	private RTHitpointObjMap hitpointObj = new RTHitpointObjMap();
	/**
	 * the nextHitpointObj used in getValidHitPoint function
	 */
	private RTHitpointObjMap nextHitpointObj = new RTHitpointObjMap();

	/**
	 * Constructor of RTCoreTracing
	 * 
	 * @param pixelList
	 *            a pixel array to be calculated as a unit by a single thread
	 * @param importXML
	 *            instance of RTImportXML, being used to store world scene
	 *            parameters
	 * @param result
	 *            the result bitmap
	 */
	public RTCoreTracing(Point[] pixelList, RTImportXML importXML,
			RTBitmap result,RTCanvas c) {
		if (camera == null) {
			camera = importXML.getCamera();
		}
		if (lightList == null) {
			lightList = importXML.getLightList();
		}
		if (objList == null) {
			objList = importXML.getObjectList();
		}
		if (background == null) {
			background = importXML.getBackground();
		}
		this.pixelCoordinationArray = pixelList;
		if (canvas == null) {
			canvas = c;
		}
		resultBitmap = result;
	}

	/**
	 * This is recursive function for calculating pixel color, the recursive
	 * depth is depended on the depth of reflection.
	 * 
	 * @param currentRay
	 *            the current ray list
	 * @param objList
	 *            the objects in the world
	 * @param recursiveDepth
	 *            recursive level
	 * @return a color after computing shading and used recursively in
	 *         reflection
	 */
	private synchronized RTColor calPixelColorAux(final RTRay currentRay,
			final Vector<RTIObject> aObjList, int recursiveDepth) {
		RTColor result = null;
		// Test if the reflection depth exceeds the design limitation.
		if (recursiveDepth++ > canvas.getReflectionDepth()) {
			return result;
		}

		Vector<RTHitpointObjMap> hitPointsMapList = new Vector<RTHitpointObjMap>();
		hitPointsMapList = currentRay.objectListHitDetect(aObjList,
				hitPointsMapList);
		RTHitpointObjMap validHitPointObjMap = this
				.getValidHitPoint(hitPointsMapList);

		// Calculate the color of the pixel related to valid hitPoint
		if (validHitPointObjMap != null) {
			// If the ray hit an object
			RTColor originColor = validHitPointObjMap.getHitObject()
					.calOriginColor(validHitPointObjMap);
			if (originColor == null) {
				System.out.println("original color should not be null!");
			}
			RTColor shadingColor = calIlluminateColor(validHitPointObjMap,
					originColor);

			// To check if need to calculate reflection
			if (canvas.getIsShadowingReflection()
					&& validHitPointObjMap.getHitObject().getSurface()
							.getReflect() != 0.0f) {
				// We need to do reflection here
				RTVector hitPointNormal = validHitPointObjMap.getHitNormal();
				RTVector Rd = currentRay.getDirection();
				double rnRd2 = RTVector.dotProductVectors(hitPointNormal, Rd)
						* (double) 2.0;

				RTVector reflectionDirection = RTVector.sumVectors(Rd, RTVector
						.reverseVector(RTVector.multipleVectors(hitPointNormal,
								rnRd2)));
				RTVector origin = RTVector.sumVectors(validHitPointObjMap
						.getHitPoint(), RTVector.multipleVectors(
						hitPointNormal, 0.0001f));
				RTRay reflectionRay = new RTRay(origin, RTVector.sumVectors(
						origin, reflectionDirection));

				RTColor reflectionColor = this.calPixelColorAux(reflectionRay,
						objList, recursiveDepth);

				if (reflectionColor != null) {
					// If the returned reflection color isn't null. Then the
					// reflected ray hit some object.
					double reflect = validHitPointObjMap.getHitObject()
							.getSurface().getReflect();
					result = RTColor.combineColor(RTColor.multByNumber(
							shadingColor, 1.0 - reflect), RTColor.multByNumber(
							reflectionColor, reflect));
				} else {
					result = shadingColor;
				}
			} else {
				result = shadingColor;
			}
		}
		return result;
	}

	/**
	 * This method is used to compare a hit Points list and calculate the valid
	 * hit point(the one near camera). Returns a RTHitpointObjMap that contains
	 * details of the valid hit point.
	 * 
	 * @return the map between hit point and image
	 * @param hitPointsMapList
	 *            the mapList
	 */
	private RTHitpointObjMap getValidHitPoint(
			final Vector<RTHitpointObjMap> hitPointsMapList) {
		if (hitPointsMapList.isEmpty()) {
			return null;
		}
		cameraLocation = camera.getLocationVector();
		hitpointObj = hitPointsMapList.get(0);
		nextHitpointObj = hitpointObj;
		double shortestLen = RTVector.calVectorLength(RTVector.sumVectors(
				hitpointObj.getHitPoint(), RTVector
						.reverseVector(cameraLocation)));
		double nextPointLen = shortestLen;
		for (int i = 1; i < hitPointsMapList.size(); i++) {
			nextHitpointObj = hitPointsMapList.get(i);
			nextPointLen = RTVector.calVectorLength(RTVector.sumVectors(
					nextHitpointObj.getHitPoint(), RTVector
							.reverseVector(cameraLocation)));
			if (nextPointLen < shortestLen) {
				hitpointObj = nextHitpointObj;
				shortestLen = nextPointLen;
			}
		}
		return hitpointObj;
	}

	/**
	 * This method is used to calculate the color of a hit point under
	 * illumination.
	 * 
	 * @param validHitPointObjMap
	 *            the valid point
	 * @param originColor
	 *            the color need to calculate cal illuminate
	 * @return the result color with illumination
	 */

	private RTColor calIlluminateColor(
			final RTHitpointObjMap validHitPointObjMap,
			final RTColor originColor) {
		RTVector rectifyHitPoint = RTVector.sumVectors(validHitPointObjMap
				.getHitPoint(), RTVector.multipleVectors(validHitPointObjMap
				.getHitNormal(), RECTIFYER));
		// calculate the vector from hitPoint to Light Source
		double multFactor = 0.0;
		Vector<RTHitpointObjMap> shadowPointsMapList = new Vector<RTHitpointObjMap>();
		RTRay curRay = new RTRay();
		for (int i = 0; i < lightList.size(); i++) {
			double tempFactor = 0.0;
			curRay
					.setRay(rectifyHitPoint, lightList.get(i)
							.getPositionVector());
			// synchronize among multi-thread, in order to avoid changing
			// "objList" simultaneously
			// delete the valid hit object from the objList, in case it
			// will be calculated as an object causing a shadow
			synchronized (objList) {
				if (!validHitPointObjMap.getHitObject().equals(null)) {
					int temp = objList.indexOf(validHitPointObjMap
							.getHitObject());
					objList.remove(validHitPointObjMap.getHitObject());
					shadowPointsMapList = curRay.objectListHitDetect(objList,
							shadowPointsMapList);
					objList.insertElementAt(validHitPointObjMap.getHitObject(),
							temp);
				} else {
					shadowPointsMapList = curRay.objectListHitDetect(objList,
							shadowPointsMapList);
				}
			}

			// The Condition is "Not to compute shadow"
			// or "if the point is Not in the shadow"
			if (!canvas.getIsShadowingReflection()
					|| shadowPointsMapList.isEmpty()
					|| shadowPointsMapList.elementAt(0) == null
					|| (!this.isShadowsBetweenOL(shadowPointsMapList,
							validHitPointObjMap, lightList.get(i)))) {
				RTVector hitPointToLight = RTVector.normaliseVectors(RTVector
						.sumVectors(lightList.get(i).getPositionVector(),
								RTVector.reverseVector(rectifyHitPoint)));
				// calculate the cos value of angle
				// between Light-Center and
				// Hit point-Center
				double cosAngleLOH = Math.abs(RTVector.dotProductVectors(
						validHitPointObjMap.getHitNormal(), hitPointToLight));
				// calculate color value using formula
				tempFactor = cosAngleLOH
						* (validHitPointObjMap.getHitObject().getSurface()
								.getDiffuse());
				multFactor = multFactor + tempFactor;
			}

			shadowPointsMapList.removeAllElements();
		}
		// Multiply the origin color by illumination factor
		RTColor illuminateColor = RTColor.multByNumber(originColor, multFactor);

		return illuminateColor;
	}

	/**
	 * @param shadowPointsMapList
	 *            the shadowList
	 * @param validHitPointObjMap
	 *            the valid hit point
	 * @param light
	 *            the light
	 * @return whether the shadow in between light and hitpoint or not
	 */
	private boolean isShadowsBetweenOL(
			final Vector<RTHitpointObjMap> shadowPointsMapList,
			final RTHitpointObjMap validHitPointObjMap, final RTLight light) {
		RTVector leftHand = new RTVector(0, 0, 0);
		RTVector rightHand = new RTVector(0, 0, 0);

		boolean isShadowsBetweenOL = false;
		for (int i = 0; i < shadowPointsMapList.size(); i++) {
			leftHand = RTVector.sumVectors(validHitPointObjMap.getHitPoint(),
					RTVector.reverseVector(shadowPointsMapList.get(i)
							.getHitPoint()));
			rightHand = RTVector.sumVectors(light.getPositionVector(), RTVector
					.reverseVector(shadowPointsMapList.get(i).getHitPoint()));
			if (RTVector.dotProductVectors(leftHand, rightHand) <= 0.1e-6) {
				// to avoid the cos value approach to 0. checked by test3
				isShadowsBetweenOL = true;
				break;
			} else {
				isShadowsBetweenOL = false;
			}
		}
		return isShadowsBetweenOL;
	}

	public final void run() {

		RTVector xOffset = new RTVector();
		RTVector yOffset = new RTVector();
		RTVector curPoint = new RTVector();
		// denote the current ray
		RTRay curRay = new RTRay();
		// move the variable outside
		RTVector canvasRightD = canvas.getRightD();
		RTVector canvasDownD = canvas.getDownD();
		RTVector canvasCornerUL = canvas.getCornerUL();

		cameraLocation = camera.getLocationVector();
		RTColor resultColor = new RTColor(background);
		for (int i = 0; i < pixelCoordinationArray.length; i++) {

			xOffset = RTVector.multipleVectors(canvasRightD, canvas.getRx()
					* (pixelCoordinationArray[i].getX() + 0.5));
			yOffset = RTVector.multipleVectors(canvasDownD, canvas.getRx()
					* (pixelCoordinationArray[i].getY() + 0.5));
			curPoint = RTVector.sumVectors(canvasCornerUL, RTVector.sumVectors(
					xOffset, yOffset));
			curRay.setRay(cameraLocation, curPoint);
			resultColor = calPixelColorAux(curRay, objList, 0);

			if (resultColor != null) {
				resultBitmap.setColorAtPoint(pixelCoordinationArray[i],
						resultColor);
			} else {
				resultBitmap.setColorAtPoint(pixelCoordinationArray[i],
						background);
			}
		}
	}
}
